Annotation in Java
contents
현대적인 프레임워크인 Spring Boot, Hibernate, Lombok이 마치 "마법"처럼 동작하게 만드는 메커니즘이 바로 자바 애노테이션(Java Annotations) 입니다.
본질적으로 애노테이션은 메타데이터(Metadata) 입니다. 코드(클래스, 메서드, 변수)에 붙이는 "꼬리표"나 "포스트잇"이라고 생각하면 됩니다.
가장 중요한 개념: 기본적으로 애노테이션은 아무런 동작도 하지 않습니다. 수동적인 존재입니다. 컴파일러나 런타임 프레임워크(Spring 등)가 이 꼬리표를 발견하고 그에 맞는 로직을 실행할 때 비로소 기능이 작동합니다.
1. 애노테이션의 해부
애노테이션을 이해하려면 메타-애노테이션(Meta-Annotations) 을 알아야 합니다. 이는 다른 애노테이션을 설명하는 애노테이션입니다.
커스텀 애노테이션을 만들 때는 두 가지 규칙을 정해야 합니다:
- 어디에 붙일 것인가? (
@Target) - 언제까지 살아있을 것인가? (
@Retention)
A. @Target (어디에?)
애노테이션의 적용 범위를 정의합니다.
ElementType.TYPE: 클래스, 인터페이스, Enum.ElementType.METHOD: 함수(메서드).ElementType.FIELD: 필드 변수.ElementType.PARAMETER: 메서드의 파라미터.
B. @Retention (언제까지?)
성능과 동작 방식에 가장 큰 영향을 미치는 설정입니다.
| 정책 | 생명주기 (Lifecycle) | 누가 보는가? | 예시 |
|---|---|---|---|
| SOURCE | 컴파일 후 사라짐. | 컴파일러(javac)만 봄. |
Lombok (@Getter), @Override |
| CLASS | .class 파일에 남지만 JVM은 무시. |
바이트코드 툴. | @NonNull (일부 경우) |
| RUNTIME | JVM 메모리까지 로드됨. | **리플렉션(Reflection)**이 볼 수 있음. | Spring (@Service), JPA (@Entity) |
2. 커스텀 애노테이션 만들기
@LogExecutionTime이라는 애노테이션을 만든다고 가정해 봅시다.
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 메서드에만 붙일 수 있음
@Retention(RetentionPolicy.RUNTIME) // 런타임에 리플렉션으로 읽어야 함
public @interface LogExecutionTime {
// 속성 (메서드처럼 생겼지만 변수처럼 씀)
String value() default ""; // 'value'는 특별한 이름입니다
boolean active() default true;
}
사용법:
public class MyService {
@LogExecutionTime(value = "Checking User", active = true)
public void processUser() {
// ... 로직
}
}
3. "마법"의 원리: 처리 과정
앞서 말했듯, 위 애노테이션만으로는 아무 일도 일어나지 않습니다. 실제로 실행 시간을 로그로 남기는 엔진은 두 가지 방식이 있습니다.
엔진 A: 런타임 처리 (Reflection)
Spring이 작동하는 방식입니다.
- 앱 시작: Spring이 패키지 내의 모든 클래스를 스캔합니다.
- 감지:
method.isAnnotationPresent()를 사용해 "이 메서드에@LogExecutionTime이 붙어있나?"라고 확인합니다. - 프록시: 만약 붙어있다면, Spring은 해당 클래스를 프록시(Proxy) 로 감쌉니다.
- 실행: 메서드를 호출하면 프록시가 가로채서 타이머를 시작하고, 실제 메서드를 실행한 뒤, 타이머를 멈추고 로그를 찍습니다.
엔진 B: 컴파일 타임 처리 (APT)
Lombok(@Getter, @Setter)이 작동하는 방식입니다.
- 컴파일 (
javac): 컴파일러가.java파일을 구문 분석하여 트리 구조(AST)를 만듭니다. - 프로세서: 애노테이션 프로세서(Annotation Processor) 가 실행되어
@Getter를 발견합니다. - 코드 생성:
public String getName() { ... }에 해당하는 바이트코드를 컴파일된.class파일에 물리적으로 주입합니다. - 결과: 소스 코드에는 없던 메서드가
.class파일에는 존재하게 됩니다. 이것이 롬복이RetentionPolicy.SOURCE를 사용하는 이유입니다.
4. 자바 내장 애노테이션
컴파일러 안전성을 위해 매일 사용하는 것들입니다.
@Override: "부모 메서드를 덮어쓸 의도입니다"라고 컴파일러에게 알립니다. 메서드 이름에 오타가 있으면 컴파일 에러를 띄워줍니다.@Deprecated: 더 이상 쓰지 않는 메서드임을 표시합니다. 사용 시 경고가 뜹니다.@SuppressWarnings: 컴파일러 경고(예: "unchecked cast")를 무시하게 만듭니다.@FunctionalInterface: 인터페이스가 정확히 하나의 추상 메서드만 가지도록 강제합니다 (람다용).
5. 심화: 합성 애노테이션 (Spring 방식)
Spring은 애노테이션을 결합하는 것을 허용합니다. 이를 스테레오타입(Stereotypes) 이라 합니다.
매번 이렇게 쓰는 대신:
@Component
@ResponseBody
@RequestMapping("/api")
public class MyController { ... }
Spring은 @RestController를 정의할 때 그 안에 이미 @Controller와 @ResponseBody를 포함시켜 둡니다. 덕분에 코드가 훨씬 깔끔해집니다.
6. 요약: 언제 무엇을 쓰는가?
| 특징 | 소스 / 컴파일 타임 (APT) | 런타임 (Reflection) |
|---|---|---|
| 속도 | ⚡️ 매우 빠름 (런타임 비용 0) | 🐢 느림 (시동 시 스캔 + 리플렉션 오버헤드) |
| 용도 | 코드 생성, 보일러플레이트 제거. | 의존성 주입, AOP, 유효성 검사. |
| 예시 | Lombok, MapStruct, Dagger2. | Spring Boot, Hibernate, JUnit. |
| 난이도 | 높음 (프로세서 직접 짜기 어려움). | 중간 (Aspect 짜기 쉬움). |
references